【SwiftUI】プレースホルダー付きのTextEditorを自作してみた
SwiftUIのTextEditor
にプレースホルダーを付けたかったのですが、標準のAPIではプレースホルダーがありませんでした。良い解決方法がなかなか見当たらず、試行錯誤しながら自作してみることにしました。
作ったもの
環境
- Xcode 13.3
- iOS 15.4
TextEditorWithPlaceholder
こちらがプレースホルダー付きのTextEditor
になります。
import SwiftUI struct TextEditorWithPlaceholder: View { @FocusState private var focusedField: Field? enum Field { case textEditor case placeholder } @Binding var text: String private let placeholderText: String init(_ placeholder: String, text: Binding<String>) { self._text = text self.placeholderText = placeholder } var body: some View { ZStack { // テキストが空の時にプレースホルダーを表示する if text.isEmpty { ZStack { Rectangle() .fill(Color(uiColor: .systemBackground)) .onTapGesture { focusedField = .placeholder } VStack { HStack { TextField(placeholderText, text: $text) .focused($focusedField, equals: .placeholder) .onAppear { focusedField = .placeholder } Spacer() } .padding(.leading, 6) .padding(.top, 8) Spacer() } } // テキストが空ではない時にTextEditorを表示する } else { TextEditor(text: $text) .focused($focusedField, equals: .textEditor) .onAppear { focusedField = .textEditor } } } } }
FocusState
今回はFocusState
を用いて、プレースホルダー用のTextField
とTextEditor
のフォーカス状態を管理します。
@FocusState private var focusedField: Field? enum Field { case textEditor case placeholder }
init
TextFieldWithPlaceholder
生成時には、プレースホルダー用のテキストと、バインディングするテキストを設定します。
@Binding var text: String private let placeholderText: String init(_ placeholder: String, text: Binding<String>) { self._text = text self.placeholderText = placeholder }
body
今回はテキストが空の時にはTextField
を表示して、テキストが空ではない時はTextEditor
を表示するようにしました。テキストが空の時にTextField
を表示するのは、TextField
のプレースホルダーをそのまま使用する為です。
テキストが空の時にはTextFieldを表示
if text.isEmpty { ZStack { Rectangle() // TextEditorっぽく見せる為に背景を設置 .fill(Color(uiColor: .systemBackground)) // 背景がタップされた時にもプレースホルダー用TextFieldにフォーカスを当てる .onTapGesture { focusedField = .placeholder } VStack { HStack { TextField(placeholderText, text: $text) // FocusStateで.placeholderでフォーカスが当たるようにする .focused($focusedField, equals: .placeholder) // テキストが空になり、TextFieldが表示された時にフォーカスが当たるようにする .onAppear { focusedField = .placeholder } Spacer() } .padding(.leading, 6) .padding(.top, 8) Spacer() } }
まずTextField
をTextEditor
っぽく見せる為に背景を設置しています。
TextField
の.focused($focusedField, equals: .placeholder)
で.placeholder
がfocusedField
に代入された時にこのTextField
にフォーカスが当たるようにしています。そして、こちらの背景がタップされた時にTextField
にフォーカスが当たるように実装しました。
また、テキストが空ではない状態から空になった時にも、TextField
にフォーカスが当たる状態にしたかった為、onAppear
時にもフォーカスされるようにしました。
.padding
でプレースホルダーの見え方がTextEditor
と同じように見えるように調整しています。
テキストが空では無い時にTextEditorを表示
} else { TextEditor(text: $text) // FocusStateで.textEditorでフォーカスが当たるようにする .focused($focusedField, equals: .textEditor) // テキストが空で無くなり、TextEditorが表示された時にフォーカスが当たるようにする .onAppear { focusedField = .textEditor } }
.focused($focusedField, equals: .textEditor)
で.textEditor
がfocusedField
に代入された時にこのTextEditor
にフォーカスが当たるようにしています。
また、テキストが空では無くなるとこのTextEditor
が表示されるのですが、その時にTextEditor
にフォーカスが当たる状態にしたかった為、onAppear
時にもフォーカスが当たるようにしています。
使い方
自作したプレースホルダー付きTextEditor
は、第一引数にプレースホルダーを渡して、第二引数にバインディングするテキストを渡して使います。
TextEditorWithPlaceholder("Write down...", text: $text)
ContentView
今回のサンプルデモで実装したContentView
は下記の内容になります。
struct ContentView: View { @FocusState private var isFocusedTextEditor: Bool @State private var text: String = "" var body: some View { ZStack { Rectangle() .fill(.yellow) .ignoresSafeArea() VStack { Text("Memo pad") .font(.title) TextEditorWithPlaceholder("Write down...", text: $text) .focused($isFocusedTextEditor) } .padding() } .onTapGesture { isFocusedTextEditor = false } } }
今回のコード
GitHubに上げております。
おわりに
プレースホルダー付きのTextEditor
を自作することは出来ましたが、かなり強引な方法で作成しました。そもそも標準で提供していないので、TextEditor
にはプレースホルダーは必要のないという見解なのか、それとも今後いつか追加されるのか興味津々です。こんなに強引に実装しなくてもより良い方法があるよ等ありましたら教えていただけると幸いです。
さぁ、今年のWWDCもいよいよですね。どんな楽しいニュースが待っているのでしょうか?
モバイルアプリ開発のチームメンバー絶賛募集中!
モバイル事業部では事業会社様と一緒に、数年間にわたり長期でモバイルアプリをグロースさせています。
そんなモバイルアプリ開発のチームメンバーを絶賛募集中です!
もちろんモバイルアプリ開発以外のエンジニアも募集中です!